﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using DependencyAnalysis.DataModel;

namespace DependencyAnalysis.Internal
{
    public class CilParser
    {
        private static OpCode[] _singleByteOpCode;
        private static OpCode[] _doubleByteOpCode;
        private byte[] _instructions;
        private IList<LocalVariableInfo> _locals;
        private ParameterInfo[] _parameters;
        private Type[] _typeArgs;
        private Type[] _methodArgs;
        private MethodBase _currentMethod;
        private List<Instruction> _ilInstructions;

        public int CurrentVariableCount { get; private set; }

        public ParameterInfo[] Parameters { get { return _parameters; } }

        public bool IgnoreNop { get; set; }

        static CilParser()
        {
            CreateOpCodes();
        }

        private static void CreateOpCodes()
        {
            _singleByteOpCode = new OpCode[225];
            _doubleByteOpCode = new OpCode[31];

            IEnumerable<FieldInfo> fields = GetOpCodeFields();

            foreach (FieldInfo fieldInfo in fields)
            {
                OpCode code = (OpCode)fieldInfo.GetValue(null);

                if (code.OpCodeType == OpCodeType.Nternal)
                    continue;

                if (code.Size == 1)
                    _singleByteOpCode[code.Value] = code;
                else
                    _doubleByteOpCode[code.Value & 0xff] = code;
            }
        }

        private static HashSet<OperandType> ComplexOperands = new HashSet<OperandType>
        {
            OperandType.InlineTok,
            OperandType.InlineType,
            OperandType.InlineMethod,
            OperandType.InlineField,
            OperandType.ShortInlineVar,
            OperandType.InlineVar
        };

        public List<Instruction> ReadInstructions(MethodBase method)
        {
            _currentMethod = method;
            MethodBody methodBody;
            try
            {
                methodBody = _currentMethod.GetMethodBody();
            }
            catch (TypeLoadException)
            {
                methodBody = null;
            }
            if (methodBody == null)
            {
                _ilInstructions = new List<Instruction>();
                return _ilInstructions;
            }

            _locals = methodBody.LocalVariables;
            CurrentVariableCount = _locals.Count;
            _instructions = methodBody.GetILAsByteArray();
            _ilInstructions = new List<Instruction>(_instructions.Length / 2);
            _parameters = method.GetParameters();
            try
            {
                if (!(method.GetType() == typeof(ConstructorInfo)) && method.IsGenericMethod)
                    _methodArgs = method.GetGenericArguments();
            }
            catch (NotSupportedException)
            {
            }

            try
            {
                if (method.DeclaringType != null && method.DeclaringType.IsGenericType)
                    _typeArgs = method.DeclaringType.GetGenericArguments();
            }
            catch (NotSupportedException)
            {
            }
            using (MemoryStream stream = new MemoryStream(_instructions))
            {
                using (BinaryReader reader = new BinaryReader(stream, Encoding.UTF8))
                {
                    while (stream.Position < stream.Length)
                    {
                        var instruction = new Instruction { Offset = stream.Position };
                        byte instructionByte = reader.ReadByte();
                        OpCode code = instructionByte != 254
                                          ? _singleByteOpCode[instructionByte]
                                          : _doubleByteOpCode[reader.ReadByte()];

                        try
                        {
                            instruction.OperandNonSerialized = ReadOperand(code, method.Module, reader);
                            if (!ComplexOperands.Contains(code.OperandType))
                            {
                                instruction.Operand = instruction.OperandNonSerialized;
                            }
                        }
                        catch (TypeLoadException)
                        {
                        }
                        if (code.Size == 1)
                            instruction.OpCode = (OperationCode)code.Value;
                        else
                            instruction.OpCode = (OperationCode)(0xffff + 1 + code.Value);
                        if (IgnoreNop && instruction.OpCode == OperationCode.Nop)
                            continue;
                        _ilInstructions.Add(instruction);
                    }
                }
            }
            return _ilInstructions;
        }

        private object ReadOperand(OpCode code, Module module, BinaryReader reader)
        {
            Stream stream = reader.BaseStream;
            object operand = null;

            switch (code.OperandType)
            {
                case OperandType.InlineNone:
                    break;
                case OperandType.InlineSwitch:
                    int length = reader.ReadInt32();
                    int[] branches = new int[length];
                    int[] offsets = new int[length];
                    for (int i = 0; i < length; i++)
                        offsets[i] = reader.ReadInt32();
                    for (int i = 0; i < length; i++)
                        branches[i] = (int)stream.Position + offsets[i];
                    break;
                case OperandType.ShortInlineBrTarget:
                    operand = (reader.ReadSByte() + stream.Position);
                    break;
                case OperandType.InlineBrTarget:
                    operand = (reader.ReadInt32() + stream.Position);
                    break;
                case OperandType.ShortInlineI:
                    if (code == OpCodes.Ldc_I4_S)
                        operand = (sbyte)reader.ReadByte();
                    else
                        operand = reader.ReadByte();
                    break;
                case OperandType.InlineI:
                    operand = reader.ReadInt32();
                    break;
                case OperandType.ShortInlineR:
                    operand = reader.ReadSingle();
                    break;
                case OperandType.InlineR:
                    operand = reader.ReadDouble();
                    break;
                case OperandType.InlineI8:
                    operand = reader.ReadInt64();
                    break;
                case OperandType.InlineSig:
                    operand = module.ResolveSignature(reader.ReadInt32());
                    break;
                case OperandType.InlineString:
                    operand = module.ResolveString(reader.ReadInt32());
                    break;
                case OperandType.InlineTok:
                case OperandType.InlineType:
                case OperandType.InlineMethod:
                case OperandType.InlineField:
                    operand = module.ResolveMember(reader.ReadInt32()
                        , _typeArgs, _methodArgs);
                    break;
                case OperandType.ShortInlineVar:
                    operand = GetVariable(code, reader.ReadByte());
                    break;
                case OperandType.InlineVar:
                    operand = GetVariable(code, reader.ReadUInt16());
                    break;
                default:
                    throw new NotSupportedException();
            }

            return operand;
        }

        private object GetVariable(OpCode code, int index)
        {
            if (code.Name.Contains("loc"))
                return _locals[index];

            if (!_currentMethod.IsStatic)
                index--;

            return _parameters[index];
        }

        private static IEnumerable<FieldInfo> GetOpCodeFields()
        {
            return typeof(OpCodes).GetFields(BindingFlags.Public
                | BindingFlags.Static);
        }
    }
}
